//------------------------------------------------------------------------------
// X2D Game Engine
// Copyright (C) 2006-2007 Janusz Kowalski
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//------------------------------------------------------------------------------

#include <png.h>
#include <cstdio>
#include <cassert>
#include <cstring>

#include "imgSDL.h"
#include "RawVector.h"

#include "pmsg.h"
#include "os.h"

// Load PNG image from the given file.
// Returns: 0 - some problem, anything else - success.
static SDL_Surface *LoadPNG(const char *path) {
  FILE *file = fileOpenRead(path);

  unsigned char sig[8];
  fread(sig,1,8,file);
  assert(png_check_sig(sig, 8));  // Check the file signature
  
  png_structp png_ptr;
  
  png_ptr=png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);
  if(png_ptr==0) {
    fclose(file);
    return 0;
  }

  png_infop info_ptr;

  info_ptr=png_create_info_struct(png_ptr);
  if(info_ptr==0) {
    png_destroy_read_struct(&png_ptr,0,0);
    fclose(file);
    return 0;
  }
  
  // Set the target for longjump (HERE)
  if(setjmp(png_ptr->jmpbuf)) {
    png_destroy_read_struct(&png_ptr, &info_ptr, 0);
    fclose(file);
    return 0;
  }
  
  png_init_io(png_ptr, file);
  png_set_sig_bytes(png_ptr, 8);
  png_read_info(png_ptr, info_ptr);
  
  png_uint_32 width, height;

  int bit_depth, color_type;
  png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, 0, 0, 0);
  
  // Do some changes
  if (color_type == PNG_COLOR_TYPE_PALETTE)
    png_set_expand(png_ptr);
  if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
    png_set_expand(png_ptr);
  if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
    png_set_expand(png_ptr);
  
  if (bit_depth == 16)
    png_set_strip_16(png_ptr);
  if (color_type == PNG_COLOR_TYPE_GRAY ||
      color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
    png_set_gray_to_rgb(png_ptr);
  
  // Update
  png_read_update_info(png_ptr, info_ptr);
  
  int row_bytes=png_get_rowbytes(png_ptr, info_ptr);
  int channels=png_get_channels(png_ptr, info_ptr);
  
  // Update bit_depth, as it might have changed...
  bit_depth = png_get_bit_depth(png_ptr, info_ptr);

  // Allocate memory for image data;
  unsigned char *image_data = static_cast<unsigned char*>(malloc(width*height*channels));
  assert(image_data!=0);

  // Allocate memory for row pointers.
  png_bytep *row_pointer = static_cast<png_bytep*>(malloc(sizeof(png_bytep)*height));
  assert(row_pointer!=0);

  // Init row pointers.
  for(png_uint_32 i=0; i<height; i++)
    row_pointer[i]=image_data+row_bytes*i;
  
  png_read_image(png_ptr, row_pointer);

  // Free row pointers.
  free(row_pointer);

  png_read_end(png_ptr, 0);
  
  png_destroy_read_struct(&png_ptr, &info_ptr, 0);
  
  SDL_Surface *surf=0;
  assert(channels==3 || channels==4);
  unsigned int amask=(channels==4?0xff000000:0x00000000);
  surf=SDL_CreateRGBSurfaceFrom(image_data, width, height, channels*bit_depth, row_bytes, 0x000000ff, 0x0000ff00, 0x00ff0000, amask);

  // Now we make a copy of the surface.
  SDL_Surface *copy = SDL_ConvertSurface(surf, surf->format,0);

  // Free the suface.
  SDL_FreeSurface(surf);

  // Free image data (as it is not freed by SDL_FreeSurface() for surfaces created using SDL_CreateRGBSurfaceFrom.
  free(image_data);

  fclose(file);

  // The following is needed for some reason...
  if(channels==3) SDL_SetAlpha(copy, 0, SDL_ALPHA_OPAQUE);
  else SDL_SetAlpha(copy, SDL_SRCALPHA, SDL_ALPHA_OPAQUE);  // (channels==4)

  return copy;
}

// !! Very similar name (LoadImage) is defined in windows.h.
SDL_Surface *loadImage(const char *path, bool colorkey, bool optim) {
  // Check the file extension
  ASSURE(strlen(path)>4); // Path cannot be too short.
  const char *ext=path+strlen(path)-4;
  SDL_Surface *tmp_s=0;
  if(strcmp(ext, ".bmp")==0) {
    tmp_s=SDL_LoadBMP(path);
  } else if(strcmp(ext, ".png")==0) {
    tmp_s=LoadPNG(path);
  } else ASSURE(false);
  
  if(tmp_s==0) return 0;
  
  if(colorkey) SDL_SetColorKey(tmp_s, SDL_SRCCOLORKEY, SDL_MapRGB(tmp_s->format, 0xff, 0x00, 0xff));
  
  if(optim) {
    SDL_Surface *new_s=SDL_DisplayFormat(tmp_s);
    SDL_FreeSurface(tmp_s);
    return new_s;
  }
  
  return tmp_s;
}

//------------------------------------------------------------------------------
// Simple object taking care of string.
//------------------------------------------------------------------------------

class Str {
  private:
    char *s_;
  public:
    Str(const char *s) {
      ASSURE(s!=NULL);
      s_ = new char[strlen(s)+1];
      unsigned i=0; do s_[i]=s[i]; while(s[i++]);
    }
    Str(Str &src) {
      if(&src==this) return;
      s_=src.s_; src.s_=0;
    }
    bool operator==(const char *s) {
      if(strcmp(s_,s)==0) return true;
      else return false;
    }
    ~Str() {
      delete[] s_;
    }
};

//------------------------------------------------------------------------------
// ResourceList template.
//------------------------------------------------------------------------------

// R - Type of resource handlers/descriptors.
template <class R, class D> class ResList {
  private:
    static RawVector<Str> keys_;
    static RawVector<R> ress_;
    static RawVector<int> counters_;
  public:
    static int indexOf(const char *key) {
      for(int i=keys_.size(); i--;) {
        if(keys_[i]==key) return i;
      }
      return -1;
    }
    static int indexOf(D data) {
      for(int i=ress_.size(); i--;) {
        if(ress_[i]==data) return i;
      }
      return -1;
    };
    static void inc(int i) {
      counters_[i]++;
    }
    static void dec(int i) {
      counters_[i]--;
      if(counters_[i]==0) {
        keys_.removeElementAt(i); // Calls key destructor.
        ress_.removeElementAt(i); // Calls data destructor.
        counters_.removeElementAt(i);
      }
    }
    
    // Returns index of newly created resource.
    static int create(const char *key) {
      Str s(key); R r(key);
      keys_.add(s); ress_.add(r); counters_.add(1);
      return ress_.size()-1;
    }
    static D getData(int index) {
      return ress_[index].getData();
    }
};

template <class R, class D> RawVector<Str> ResList<R, D>::keys_;
template <class R, class D> RawVector<R> ResList<R, D>::ress_;
template <class R, class D> RawVector<int> ResList<R, D>::counters_;

//------------------------------------------------------------------------------
// ResOptSDLSurf - SDL_Surface optimized for on-screen display - Resource
//------------------------------------------------------------------------------

class ResOptSDLSurf {
  private:
    SDL_Surface *surf_;
  public:
    ResOptSDLSurf(const char *path) {
      surf_ = loadImage(path, true, true);
      if(surf_==0) throw "ERROR: Image cannot be loaded!";
    }
    ResOptSDLSurf(ResOptSDLSurf& src) {
      surf_=src.surf_; src.surf_=0;
    }
    bool operator==(SDL_Surface *surf) {
      if(surf_==surf) return true; else return false;
    }
    ~ResOptSDLSurf() {
      // !! Is this condition necessary?
      if(surf_!=0) SDL_FreeSurface(surf_);
    }
    inline SDL_Surface* getData() {
      return surf_;
    }
};

//------------------------------------------------------------------------------
// ResGLTex - OpenGL texture name - Resource
//------------------------------------------------------------------------------

inline int nextPow2(int n) {
  // !! We should check the size of int.
  n--; n|=n>>1; n|=n>>2; n|=n>>4; n|=n>>8; n|=n>>16; n++; return n;
}

inline bool isPow2(int n) {
  if((n&(n-1))!=0) return false;
  return true;
}

#ifdef X2D_WITH_OPENGL
class ResGLTex {
  private:
    GLTex *glTex_;
  public:
    ResGLTex(const char *path) {
      SDL_Surface *surf = loadImage(path, false, false);
      
      // Now we create RGBA surface (however we could start with RGBA surface).
      // !! We could probably create such a surface when loading image.
      
      // Prepare format information.
      SDL_PixelFormat f;
      f.palette=0;
      f.BitsPerPixel=32;
      f.BytesPerPixel=4;
      f.Rloss=0; f.Rloss=0; f.Bloss=0; f.Aloss=0;
      
      #if SDL_BYTEORDER == SDL_BIG_ENDIAN
        f.Rmask = 0xff000000; f.Gmask = 0x00ff0000;
        f.Bmask = 0x0000ff00; f.Amask = 0x000000ff;
        r.Rshift=24; f.Gshift=16; f.Bshift=8; f.Ashift=0;
      #else
        f.Rmask = 0x000000ff; f.Gmask = 0x0000ff00;
        f.Bmask = 0x00ff0000; f.Amask = 0xff000000;
        f.Rshift=0; f.Gshift=8; f.Bshift=16; f.Ashift=24;
      #endif
      
      f.colorkey=0;
      f.alpha=255;
      
      SDL_Surface *surfRGBA = SDL_ConvertSurface(surf, &f, SDL_SWSURFACE);
      SDL_FreeSurface(surf);
      
      // We apply colorkey replacing every occurence of #FF00FF color with alpha 0.
      SDL_LockSurface(surfRGBA);
      Uint32 *p = static_cast<Uint32*>(surfRGBA->pixels);
      Uint32 *lastp = p + surfRGBA->pitch / 4 * surfRGBA->h;
      for(;p!=lastp;p++) {
      #if SDL_BYTEORDER == SDL_BIG_ENDIAN
        if(*p==0xFF00FFFF) *p=0xFF00FF00;
      #else
        if(*p==0xFFFF00FF) *p=0x00FF0000;
      #endif
      }
      SDL_UnlockSurface(surfRGBA);
      
      // Correct dimensions.
      if(!isPow2(surfRGBA->w) || !isPow2(surfRGBA->h)) {
        SDL_Surface *tmp = surfRGBA;
        int w = nextPow2(tmp->w), h = nextPow2(tmp->h);
        pmsgDebugF("Dim. correction. (%d,%d) -> (%d,%d)\n", tmp->w, tmp->h, w, h);
        surfRGBA = SDL_CreateRGBSurface(SDL_SWSURFACE, w, h, 32, f.Rmask, f.Gmask, f.Bmask, f.Amask);
        SDL_LockSurface(surfRGBA);
        SDL_LockSurface(tmp);
        Uint32 *pSrc = static_cast<Uint32*>(tmp->pixels);
        Uint32 *pDst = static_cast<Uint32*>(surfRGBA->pixels);
        for(int y=0; y<h; y++) {
          for(int x=0; x<w; x++) {
            if(x<tmp->w && y<tmp->h) pDst[y*w+x]=pSrc[y*tmp->w+x];
            else pDst[y*w+x]=0;
          }
        }
        SDL_UnlockSurface(tmp);
        SDL_UnlockSurface(surfRGBA);
        SDL_FreeSurface(tmp);
      }
      
      glTex_ = new GLTex; glTex_->w=surfRGBA->w; glTex_->h=surfRGBA->h;
      glGenTextures(1, &glTex_->tex);
      glBindTexture(GL_TEXTURE_2D, glTex_->tex);
      
      // Set the texture's stretching properties
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
      
      // Set the texture object's image data using SDL_Surface's data.
      glTexImage2D(GL_TEXTURE_2D, 0, 4, surfRGBA->w, surfRGBA->h, 0,
      GL_RGBA, GL_UNSIGNED_BYTE, surfRGBA->pixels);
      
      pmsgDebugF("Texture %d created from (%s)\n", (int)glTex_->tex, path);
    }
    ResGLTex(ResGLTex &src) {
     glTex_=src.glTex_; src.glTex_=0;
    }
    ~ResGLTex() {
      if(glTex_!=0) {
        glDeleteTextures(1, &glTex_->tex);
        pmsgDebugF("Texture %d destroyed\n", (int)glTex_->tex);
        delete glTex_;
      }
    }
    inline GLTex *getData() {
      return glTex_;
    }
    bool operator==(GLTex *glTex) {
      if(glTex_==glTex) return true; else return false;
    }
};
#endif

//------------------------------------------------------------------------------
// Image
//------------------------------------------------------------------------------

#define RL_SDL ResList<ResOptSDLSurf, SDL_Surface*>
#define RL_OGL ResList<ResGLTex, GLTex*>

Image::Image() {
#ifdef X2D_WITH_OPENGL
  if((SDL_GetVideoSurface()->flags & SDL_OPENGL)==0) {
#endif
    data_.surf=0;
#ifdef X2D_WITH_OPENGL
  } else {
    data_.glTex=0;
  }
#endif
}

Image::Image(const char *path) {
#ifdef X2D_WITH_OPENGL
  if((SDL_GetVideoSurface()->flags & SDL_OPENGL)==0) {
#endif
    int index = RL_SDL::indexOf(path);
    if(index<0) index=RL_SDL::create(path); else RL_SDL::inc(index);
    data_.surf=RL_SDL::getData(index);
#ifdef X2D_WITH_OPENGL
  } else {
    int index = RL_OGL::indexOf(path);
    if(index<0) index=RL_OGL::create(path); else RL_OGL::inc(index);
    data_.glTex=RL_OGL::getData(index);
  }
#endif
}

Image::Image(const Image &src) {
#ifdef X2D_WITH_OPENGL
  if((SDL_GetVideoSurface()->flags & SDL_OPENGL)==0) {
#endif
    data_.surf=src.data_.surf; RL_SDL::inc(RL_SDL::indexOf(data_.surf));
#ifdef X2D_WITH_OPENGL
  } else {
    data_.glTex=src.data_.glTex; RL_OGL::inc(RL_OGL::indexOf(data_.glTex));
  }
#endif
}

Image &Image::operator=(const Image &right) {
  if(&right == this) return *this;
#ifdef X2D_WITH_OPENGL
  if((SDL_GetVideoSurface()->flags & SDL_OPENGL)==0) {
#endif
    if(data_.surf!=0) RL_SDL::dec(RL_SDL::indexOf(data_.surf));
    data_.surf=right.data_.surf; RL_SDL::inc(RL_SDL::indexOf(data_.surf));
#ifdef X2D_WITH_OPENGL
  } else {
    if(data_.glTex!=0) RL_OGL::dec(RL_OGL::indexOf(data_.glTex));
    data_.glTex=right.data_.glTex; RL_OGL::inc(RL_OGL::indexOf(data_.glTex));
  }
#endif
  return *this;
}

Image::~Image() {
#ifdef X2D_WITH_OPENGL
  if((SDL_GetVideoSurface()->flags & SDL_OPENGL)==0) {
#endif
    RL_SDL::dec(RL_SDL::indexOf(data_.surf));
#ifdef X2D_WITH_OPENGL
  } else {
    RL_OGL::dec(RL_OGL::indexOf(data_.glTex));
  }
#endif
}

int Image::getW() {
#ifdef X2D_WITH_OPENGL
  if((SDL_GetVideoSurface()->flags & SDL_OPENGL)==0) {
#endif
    return data_.surf->w;
#ifdef X2D_WITH_OPENGL
  } else {
    return data_.glTex->w;
  }
#endif
}

int Image::getH() {
#ifdef X2D_WITH_OPENGL
  if((SDL_GetVideoSurface()->flags & SDL_OPENGL)==0) {
#endif
    return data_.surf->w;
#ifdef X2D_WITH_OPENGL
  } else {
    return data_.glTex->h;
  }
#endif
}

void getImageWH(const char *path, int &w, int &h) {
  // !!!! Really nasty way of doing it.
  SDL_Surface *surf = loadImage(path, false, false);
  w=surf->w; h=surf->h;
  SDL_FreeSurface(surf);
}
